<template>
  <!-- 模板完全不变 -->
  <div class="viewer-wrapper">
    <div class="toolbar">
      <button @click="goPage(currentPage - 1)">上一页</button>
      <input
        v-model.number="jumpPage"
        type="number"
        min="1"
        :max="totalPages"
        @keyup.enter="goPage(jumpPage)"
        style="width: 60px; text-align: center" />
      <span>/ {{ totalPages }}</span>
      <button @click="goPage(currentPage + 1)">下一页</button>

      <label style="margin-left: 12px">缩放</label>
      <input
        v-model.number="scalePercent"
        type="number"
        min="50"
        max="300"
        step="10"
        @input="handleScaleInput"
        style="width: 70px" />
      %
    </div>

    <div class="pdf-scroll-box" @scroll="onScroll">
      <div
        v-for="p in loadedPages"
        :key="p"
        :id="`pdf-page-${p}`"
        class="page-box"
        :data-page="p">
        <canvas :ref="`canvas${p}`"></canvas>
        <div
          v-for="(blk, i) in getPageBlocks(p)"
          :key="i"
          :id="`orig-${p}-${blk.index}`"
          :class="[
            'para-box',
            {
              hover: hoverKey === `orig-${p}-${blk.index}`,
              selected: currentId === `orig-${p}-${blk.index}`,
            },
          ]"
          :style="boxStyle(blk.bbox, p)"
          @mouseenter="hoverKey = `orig-${p}-${blk.index}`"
          @mouseleave="hoverKey = null"
          @click="toggleSelect(p, blk.index)">
          <!-- <span class="label">{{ blk.index }}</span> -->
          <div
            v-show="hoverKey === `orig-${p}-${blk.index}`"
            class="tools_box flex align_items_ctr">
            <div
              v-for="(tool, toolIndex) in tools"
              :key="toolIndex"
              class="tool flex align_items_ctr jc_ctr"
              :style="toolStyle(tool)"
              @click.stop="toolClick(tool, blk)">
              <img :src="require(`@/assets/imgs/${tool.icon}.png`)" alt="" />
              <div>
                {{ tool.label }}
              </div>
            </div>
          </div>
        </div>
      </div>
      <div v-if="loadingNext" class="loading-tip">加载中…</div>
    </div>
  </div>
</template>

<script>
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
import throttle from 'lodash.throttle';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
  name: 'PdfViewer',
  props: {
    pdfName: {
      type: Number,
      default: 3,
    },
  },
  data () {
    return {
      pdf: null,
      pdfData: null,
      totalPages: 0,
      currentPage: 1,
      jumpPage: 1,
      scalePercent: 100,
      scale: 1,
      loadedPages: [],
      loadingNext: false,
      hoverKey: null,
      currentId: '',
      pageBlocks: {},
      pageViewport: {},
      renderTasks: {}, // 新增：缓存每页的渲染任务
      isProgrammaticScroll: false, //是否程序滚动-用户区分滚动监听中程序触发
      tools: [
        { label: '复制', icon: 'copy' },
        { label: '翻译', icon: 'trs' },
        { label: '收藏', icon: 'fav' },
      ],
    };
  },
  async mounted () {
    await this.loadAssets();
    this.scale = await this.calcInitScale();
    this.scalePercent = Math.round(this.scale * 100);
    this.loadedPages = this.totalPages > 1 ? [1, 2] : [1];
    this.$nextTick(() => {
      this.loadedPages.forEach((lp) => {
        this.renderPage(lp);
      });
    });
  },
  methods: {
    async loadAssets () {
      const [pdf, json] = await Promise.all([
        pdfjsLib.getDocument({
          url: `${this.pdfName}.pdf`,
          cMapUrl: '/cmaps/',
          cMapPacked: true,
        }).promise,
        fetch(`${this.pdfName}.json`).then((r) => r.json()),
      ]);
      this.pdf = pdf;
      this.pdfData = json;
      this.totalPages = pdf.numPages;
    },
    async calcInitScale () {
      const box = this.$el.querySelector('.pdf-scroll-box');
      const page = await this.pdf.getPage(1);
      const pw = page.getViewport({ scale: 1 }).width;
      return (box.clientWidth - 20) / pw;
    },
    /* 节流后的缩放入口 */
    handleScaleInput: throttle(function () {
      this.onScaleChange();
    }, 300),
    onScaleChange () {
      this.scale = this.scalePercent / 100;
      this.loadedPages.forEach((p) => this.renderPage(p));
    },
    async renderPage (pageNum) {
      /* 1. 取消上一次渲染任务（如果存在） */
      if (this.renderTasks[pageNum]) {
        this.renderTasks[pageNum].cancel();
        delete this.renderTasks[pageNum];
      }
      const page = await this.pdf.getPage(pageNum);
      const canvas = this.$refs[`canvas${pageNum}`]?.[0];
      if(!canvas) return
      const ctx = canvas.getContext('2d');
      const viewport = page.getViewport({ scale: this.scale });
      this.$set(this.pageViewport, pageNum, viewport);

      canvas.height = viewport.height;
      canvas.width = viewport.width;

      const task = page.render({ canvasContext: ctx, viewport });
      this.renderTasks[pageNum] = task;

      /* ↓↓↓ 关键：吞掉 RenderingCancelledException ↓↓↓ */
      try {
        await task.promise;
      } catch (err) {
        if (err.name === 'RenderingCancelledException') {
          // 正常取消，无需处理
          return;
        }
        // 真正的错误继续抛出
        throw err;
      } finally {
        delete this.renderTasks[pageNum];
      }

      /* 4. 缓存段落块 */
      const info = this.pdfData.pdf_info[pageNum - 1] || {};
      this.$set(this.pageBlocks, pageNum, info.para_blocks || []);
    },
    getPageBlocks (p) {
      return this.pageBlocks[p] || [];
    },
    boxStyle ([x0, y0, x1, y1], p) {
      const vp = this.pageViewport[p];
      if (!vp) return { display: 'none' };
      const s = this.scale;
      return {
        left: `${x0 * s}px`,
        top: `${y0 * s}px`,
        width: `${(x1 - x0) * s}px`,
        height: `${(y1 - y0) * s}px`,
      };
    },
    toggleSelect (p, i) {
      this.currentId = `orig-${p}-${i}`;
      // 获取当前块相对容器视口距离
      const container = document.querySelector('.pdf-scroll-box');
      const self = document.querySelector(`#${this.currentId}`);
      const parent = self.parentElement;
      const offsetTop = self.offsetTop + parent.offsetTop - container.offsetTop;
      let selfTop = offsetTop - container.scrollTop

      this.$emit(
        'blkClick',
        `#${this.currentId.replace('orig', 'trs')}`,
        false,
        selfTop
      );
    },
    goPage (p) {
      p = Math.max(1, Math.min(this.totalPages, p));
      this.currentPage = p;
      this.jumpPage = p;
      if (!this.loadedPages.includes(p)) {
        this.loadedPages.push(p);
        this.$nextTick(() => this.renderPage(p));
      }
      this.$nextTick(() => {
        const target = this.$el.querySelector(`[data-page="${p}"]`);
        if (target)
          target.scrollIntoView({ behavior: 'smooth', block: 'start' });
      });
    },
    onScroll (e) {
      if (this.isProgrammaticScroll) {
        this.isProgrammaticScroll = false;
        // 程序触发滚动不执行以下逻辑
        return;
      }
      const el = e.target;
      if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
        this.loadNext();
      }
      /* 根据页面高度和滚动高度确定当前滚动到第几页 */
      // console.log(el.scrollTop, '滚动高度');
      let heights = this.pdfData.pdf_info.map(
        (pInfo) => (pInfo.page_size[1] * this.scalePercent) / 100
      );
      heights.forEach((h, hindex) => {
        if (hindex === 0) {
          heights[hindex] = h;
        } else {
          heights[hindex] = h + heights[hindex - 1];
        }
      });
      let pageIndex = heights.findIndex((h) => {
        return el.scrollTop <= h - 200;
      });
      // console.log(pageIndex, '18444');
      // 获取当前块相对容器视口距离
      const container = document.querySelector('.pdf-scroll-box');
      const self = document.querySelector(`#pdf-page-${pageIndex + 1}`);
      const offsetTop = self.offsetTop - container.offsetTop;
      let selfTop = offsetTop - container.scrollTop
      this.$emit('blkClick', `#trs-page-${pageIndex + 1}`, true,selfTop);
    },
    loadNext () {
      if (this.loadingNext) return;
      const next = Math.max(...this.loadedPages) + 1;
      if (next > this.totalPages) return;
      this.loadingNext = true;
      this.loadedPages.push(next);
      this.$nextTick(() => {
        this.renderPage(next);
        this.loadingNext = false;
      });
    },
    // 自定义工具栏每项的宽度-防止绝对定位文字被挤压
    toolStyle (tool) {
      const { label } = tool
      if (label === '取消收藏') {
        return {
          'minWidth': '120px'
        }
      } else {
        return {
          'minWidth': '100px'
        }
      }
    },
    async toolClick (tool, blk) {
      const { label } = tool
      const { type } = blk
      console.log('当前操作的块数据', blk);
      let text = ''
      if (['text', 'title'].includes(type)) {
        // 纯文本
        blk?.lines?.forEach(line => {
          line?.spans?.forEach(span => {
            text += span.content
          })
        })
        console.log('这是当前操作的文本：', text);
      }


      if (label === '复制') {
        if (['text', 'title'].includes(type)) {
          this.$copyText(text)
        }
        if (type === 'image') {
          try {
            let imgPath = blk?.blocks?.[0]?.lines?.[0]?.spans?.[0]?.image_path
            const res = await fetch(imgPath, { mode: 'cors' });   // 1. 请求
            if (!res.ok) throw new Error('网络错误:' + res.status);
            const blob = await res.blob();                    // 2. 转 Blob
            // 使用 Clipboard API 复制图片
            await navigator.clipboard.write([
              new ClipboardItem({
                [blob.type]: blob
              })
            ]);
            this.$message.success('图片复制成功')
          } catch (error) {
            this.$message.error('图片复制失败')
          }
        }
      }
    },
  },
};
</script>

<style scoped lang="less">
/* 样式保持不变 */
.viewer-wrapper {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.toolbar {
  flex: 0 0 40px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  background: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.pdf-scroll-box {
  overflow: auto;
  padding: 10px;
  background: #fafafa;
}

.loading-tip {
  text-align: center;
  padding: 12px;
  color: #666;
}

.page-box {
  position: relative;
  margin: 0 auto 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  background: #fff;
}

.para-box {
  position: absolute;
  border: 2px solid transparent;
  pointer-events: all;
  cursor: pointer;
  background: rgba(0, 0, 255, 0);
  transition: background 0.2s;

  &.hover {
    background: rgba(135, 206, 250, 0.4);
  }

  &.selected {
    background: rgba(135, 206, 250, 0.4);
    border-color: #003d82;
  }

  .label {
    position: absolute;
    left: 2px;
    top: 2px;
    color: #fff;
    font-size: 10px;
    background: rgba(0, 0, 0, 0.3);
    padding: 0 3px;
    border-radius: 2px;
  }

  .tools_box {
    background: #fff;
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translate(-50%, 100%);
    padding: 2px;
    border-radius: 10px;
    z-index: 9999;
    box-shadow: 1px 1px 10px #ccc;

    .tool {
      padding: 10px;
      border-radius: 10px;

      >img {
        width: 24px;
        height: 24px;
        margin-right: 10px;
      }

      &:hover {
        background: #f5f5f5;
      }
    }
  }
}
</style>
